Zbudujmy model Random Forest oparty o dane FICO, dotyczące ryzyka że kredytobiorca będzie spóźniał się się z płatnościami ratalnymi.

In [63]:
import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.impute import SimpleImputer
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import train_test_split, cross_val_score, RandomizedSearchCV
from xgboost import *

def get_data():
    read = pd.read_csv("heloc_dataset_v1.csv")
    data_columns = [f for f in read.columns if f != 'RiskPerformance']
    grouped = read.groupby(data_columns)
    filtered = grouped.filter(lambda x: True)
    filtered = filtered.sample(frac=1).reset_index(drop=True)
    return filtered['RiskPerformance'], filtered[data_columns]

def prep_data():
    res, data = get_data()
    res = [1 if t == "Good" else 0 for t in res]
    for c in data.columns:
          data[c] = [-9 if t == -7 or t == -8 or t == -9 else t for t in data[c]]
    orig_data = data
    #data = imp.fit_transform(data)

    return orig_data, np.array(res), data

def get_model():
    return RandomForestClassifier(100, max_samples=800, n_jobs=-1, max_features=5)

Podzielmy zbiór na trenignowy i testowy

In [99]:
orig, res, data = prep_data()
Xtrain, Xtest, Ytrain, Ytest = train_test_split(data, res, train_size=0.8)
model = get_model()
model.fit(Xtrain, Ytrain)
Out[99]:
RandomForestClassifier(bootstrap=True, ccp_alpha=0.0, class_weight=None,
                       criterion='gini', max_depth=None, max_features=5,
                       max_leaf_nodes=None, max_samples=800,
                       min_impurity_decrease=0.0, min_impurity_split=None,
                       min_samples_leaf=1, min_samples_split=2,
                       min_weight_fraction_leaf=0.0, n_estimators=100,
                       n_jobs=-1, oob_score=False, random_state=None, verbose=0,
                       warm_start=False)

Zbadajmy poprawność tego modelu. Z AUC w okolicach 0.8 jest on wystarczający dla naszych potrzeb

In [65]:
def avg(model, x, y, eq):
    pred = model.predict(x)
    acc = [1 if eq(e,s) else 0 for (e,s) in zip(pred, y)]
    return np.average(acc)

print("Avg: " + str(avg(model, Xtest, Ytest, lambda x, y: int(round(x)) == y)))
print("Avg on train: " + str(avg(model, Xtrain, Ytrain, lambda x, y: int(round(x)) == y)))
roc_auc = roc_auc_score(Ytest, model.predict_proba(Xtest)[:, 1])
print("Roc auc: " + str(roc_auc))
Avg: 0.737093690248566
Avg on train: 0.7645512130990797
Roc auc: 0.7973023926667838

Następnie zbudujmy explainer Lime dla danych tabelarycznych, i przedstawmy wyniki wyjaśniania dla losowych instancji testowych.

In [82]:
import lime
import lime.lime_tabular

explainer = lime.lime_tabular.LimeTabularExplainer(np.array(Xtrain), feature_names=orig.columns, verbose=True)
In [95]:
def wrap_predict(model, data_x):
    ndata = pd.DataFrame(data_x, columns=orig.columns)
    return model.predict_proba(ndata)

il = [25,658,165]
for i in il:
    print("-----------------------")
    print("Expected: " + str(Ytest[i]))
    exp = explainer.explain_instance(Xtest.iloc[i], lambda d: wrap_predict(model, d), num_features=4)
    exp.show_in_notebook(show_table=True)
    for e in exp.as_list():
        print(e)
-----------------------
Expected: 1
Intercept 0.3952433726426928
Prediction_local [0.55683568]
Right: 0.57
('96.00 < PercentTradesNeverDelq <= 100.00', 0.04878659285313358)
('MSinceOldestTradeOpen > 248.00', 0.040579145173174985)
('5.00 < NetFractionRevolvingBurden <= 25.00', 0.036626396956168344)
('71.00 < ExternalRiskEstimate <= 79.00', 0.03560017071420801)
-----------------------
Expected: 0
Intercept 0.49845964266915455
Prediction_local [0.29140669]
Right: 0.12
('ExternalRiskEstimate <= 63.00', -0.07941992727023194)
('PercentTradesNeverDelq <= 87.00', -0.04831982106887372)
('AverageMInFile <= 52.00', -0.04687387579199024)
('MSinceOldestTradeOpen <= 118.00', -0.03243932543025629)
-----------------------
Expected: 1
Intercept 0.4830822465032391
Prediction_local [0.33821259]
Right: 0.31
('ExternalRiskEstimate <= 63.00', -0.0776541310339685)
('NetFractionRevolvingBurden > 54.00', -0.06899944491936026)
('MSinceMostRecentInqexcl7days > 1.00', 0.05014839568193374)
('PercentTradesNeverDelq <= 87.00', -0.048364474112274757)

Jak widać z powyższych danych najważniejsze atrybuty wskazane przez lime są dosyć konsekwentne. Widoczny jest duży wpływ zmiennej ExternalRiskEstimate, rezprezentująca zewnętrzny scoring, która w dwóch z powyższych wyjaśnień oddziałuje z największą siłą na wynik, a w trzecim przykładzie choć na czwartej pozycji, to i tak wpływa z wartością równą 70% wpływu najistotniejszej zmiennej dla tej obserwacji. We wszystkich trzech wynikach widoczna jest także zmienna PercentTradesNeverDelq mówiąca o temrinowości kredytobiorcy. Wszystko to zgadza się również z poprzednimi obserwacjami dotyczącymi tego zbioru, w tym również z wynikami analizy metodą SHAP, przedstawionymi w poprzedniej pracy.

In [106]:
xgb = XGBClassifier(verbose=0, subsample=0.8)
xgb.missing = -9
xgb.n_estimators = 25
xgb.max_depth = 8

xgb.fit(Xtrain, Ytrain)
print("Avg: " + str(avg(xgb, Xtest, Ytest, lambda x, y: int(round(x)) == y)))
print("Avg on train: " + str(avg(xgb, Xtrain, Ytrain, lambda x, y: int(round(x)) == y)))
roc_auc = roc_auc_score(Ytest, xgb.predict_proba(Xtest)[:, 1])
print("Roc auc: " + str(roc_auc))
Avg: 0.7069789674952199
Avg on train: 0.8882514640850963
Roc auc: 0.7621957809586266

Spójrzmy na porównanie z drugim modelem, stworzonym XGBoostem.

In [149]:
for i in [1697]:
    print("-----------------------")
    print("Expected: " + str(Ytest[i]))
    exp_rf = explainer.explain_instance(Xtest.iloc[i], lambda d: wrap_predict(model, d), num_features=4)
    exp_rf.show_in_notebook(show_table=True)
    exp = explainer.explain_instance(Xtest.iloc[i], lambda d: wrap_predict(xgb, d), num_features=4)
    exp.show_in_notebook(show_table=True)
-----------------------
Expected: 0
Intercept 0.42104360598399587
Prediction_local [0.54060252]
Right: 0.51
Intercept 0.34626739327105965
Prediction_local [0.45850078]
Right: 0.32617706

Spójrzmy na powyższe wyjasnienia obu modeli dla pewnej instancji. Modele zwróciły różne wyniki dla tej obserwacji, i jak widać z powyższych tabelek, trzy z czterech zmiennych uznanych za najważniejsze dla ich wyniku są różne. Co ciekawe dla tej obserwacji wspólna zmienna, ExternalRiskEstimate, wpływa z podobną wagą na wynik obu modeli.